﻿using ScottPlot;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;

namespace UtilZ.Dotnet.ScottPlotEx
{
    /// <summary>
    /// 时间图
    /// </summary>
    public class TimePlot : IPlottable
    {
        private SKPaint _paint = null;
        private SKRect _textBounds = new SKRect();

        private PlotLastDrawTarget _lastDrawTarget = PlotLastDrawTarget.Bitmap2;
        private SKBitmap _bitmap1 = null;
        private SKCanvas _bitmap1Canvas = null;

        private SKBitmap _bitmap2 = null;
        private SKCanvas _bitmap2Canvas = null;

        private int _totalStep = 0;
        private CoordinateRect _alignedExtent;

        private PlotMoveDirection _moveOrientation = PlotMoveDirection.Down;
        /// <summary>
        /// 移动方向
        /// </summary>
        public PlotMoveDirection MoveOrientation
        {
            get { return this._moveOrientation; }
        }

        private PlotDockDirection _dockDrection = PlotDockDirection.Left;
        /// <summary>
        /// 停靠方向
        /// </summary>
        public PlotDockDirection DockDrection
        {
            get { return this._dockDrection; }
        }

        private TimePlotOptions _options = null;
        /// <summary>
        /// 可选项参数
        /// </summary>
        public TimePlotOptions Options
        {
            get { return this._options; }
        }

        /// <summary>
        /// 构造函数初始化
        /// </summary>
        public TimePlot()
        {

        }

        /// <summary>
        /// 
        /// </summary>
        ~TimePlot()
        {
            this._paint?.Dispose();
            this._bitmap1?.Dispose();
            this._bitmap1Canvas?.Dispose();
            this._bitmap2?.Dispose();
            this._bitmap2Canvas?.Dispose();
        }


        /// <summary>
        /// 设置瀑布图
        /// </summary>
        /// <param name="dataWidth">数据宽带(移动方向的垂直方向时此值为FFT点数,否则为帧数)</param>
        /// <param name="dataHeight">数据高度(移动方向的水平方向时此值为FFT点数,否则为帧数)</param>
        /// <param name="moveOrientation">移动方向</param>
        /// <param name="dock">停靠方向</param>
        /// <param name="options">可选项参数</param>
        /// <exception cref="ArgumentOutOfRangeException">参数无效异常</exception>
        /// <exception cref="ArgumentException">参数无效异常</exception>
        public void Set(int dataWidth, int dataHeight, PlotMoveDirection moveOrientation, PlotDockDirection dock, TimePlotOptions options = null)
        {
            if (dataWidth < PlotConstant.SIZE_MIN)
            {
                throw new ArgumentOutOfRangeException(nameof(dataWidth), $"数据宽度不能小于{PlotConstant.SIZE_MIN}");
            }

            if (dataHeight < PlotConstant.SIZE_MIN)
            {
                throw new ArgumentOutOfRangeException(nameof(dataHeight), $"数据高度不能小于{PlotConstant.SIZE_MIN}");
            }

            switch (moveOrientation)
            {
                case PlotMoveDirection.Up:
                case PlotMoveDirection.Down:
                    if (dock != PlotDockDirection.Left && dock != PlotDockDirection.Right)
                    {
                        throw new ArgumentException($"当移动方向为垂直方向时,停靠方向只能为{nameof(PlotMoveDirection.Left)}或{nameof(PlotMoveDirection.Right)},停靠方向值\"{dock.ToString()}\"无效", nameof(dock));
                    }
                    break;
                case PlotMoveDirection.Left:
                case PlotMoveDirection.Right:
                    if (dock != PlotDockDirection.Top && dock != PlotDockDirection.Bottom)
                    {
                        throw new ArgumentException($"当移动方向为水平方向时,停靠方向只能为{nameof(PlotMoveDirection.Up)}或{nameof(PlotMoveDirection.Down)},停靠方向值\"{dock.ToString()}\"无效", nameof(dock));
                    }
                    break;
                default:
                    throw new NotSupportedException($"不支持的移动方向{this._moveOrientation.ToString()}");
            }

            if (options == null)
            {
                options = TimePlotOptions.Default;
            }


            //时间标注
            this._paint?.Dispose();
            this._bitmap1?.Dispose();
            this._bitmap1Canvas?.Dispose();
            this._bitmap2?.Dispose();
            this._bitmap2Canvas?.Dispose();

            if (options.Enable)
            {
                this._paint = new SKPaint()
                {
                    Color = options.TextColor,
                    TextSize = options.FontSize,
                    IsAntialias = options.IsAntialias,
                    TextAlign = options.TextAlign,
                    Typeface = options.Font,
                    FilterQuality = options.Smooth ? SKFilterQuality.High : SKFilterQuality.None,
                };

                DateTime testtime = DateTime.Now;
                string timeStr;
                if (options.TimeFormatFunc != null)
                {
                    timeStr = options.TimeFormatFunc(testtime);
                }
                else
                {
                    timeStr = testtime.ToString("yyyy-MM-dd HH:mm:ss");
                }
                this._paint.MeasureText(timeStr, ref this._textBounds);
                this._totalStep = 0;

                this._bitmap1 = new SKBitmap(dataWidth, dataHeight);
                this._bitmap1Canvas = new SKCanvas(this._bitmap1);

                this._bitmap2 = new SKBitmap(dataWidth, dataHeight);
                this._bitmap2Canvas = new SKCanvas(this._bitmap2);

                this._lastDrawTarget = PlotLastDrawTarget.Bitmap2;
            }
            else
            {
                this._paint = null;
                this._bitmap1 = null;
                this._bitmap1Canvas = null;
                this._bitmap2 = null;
                this._bitmap2Canvas = null;
            }

            this._alignedExtent = PlotHelper.GetAlignedExtent(options.AlignedExtentOptions, new CoordinateRect(0, dataWidth, 0, dataHeight));
            this._axisLimits = new AxisLimits(this._alignedExtent);

            this._moveOrientation = moveOrientation;
            this._dockDrection = dock;
            this._options = options;
        }



        #region IPlottable

        private bool _isVisible = true;
        /// <summary>
        /// 是否可见
        /// </summary>
        public bool IsVisible
        {
            get { return this._isVisible; }
            set { this._isVisible = value; }
        }


        private IAxes _axes = new Axes();
        /// <summary>
        /// 获取或设置坐标系
        /// </summary>
        public IAxes Axes
        {
            get { return this._axes; }
            set { this._axes = value; }
        }

        private AxisLimits _axisLimits;
        /// <summary>
        /// 获取坐标范围
        /// </summary>
        /// <returns></returns>
        public AxisLimits GetAxisLimits()
        {
            return this._axisLimits;
        }

        /// <summary>
        /// 渲染图
        /// </summary>
        /// <param name="rp"></param>
        public void Render(RenderPack rp)
        {
            if (!this._options.Enable)
            {
                return;
            }

            SKRect rect = this._axes.GetPixelRect(this._alignedExtent).ToSKRect();
            int step = this._options.GetStepFunc();
            if (step <= 0)
            {
                if (this._lastDrawTarget == PlotLastDrawTarget.Bitmap2)
                {
                    rp.Canvas.DrawBitmap(this._bitmap2, rect);
                }
                else
                {
                    rp.Canvas.DrawBitmap(this._bitmap1, rect);
                }
            }
            else
            {
                this._totalStep += step;

                if (this.IsDrawTimeText(this._totalStep))
                {
                    this._totalStep = 0;

                    DateTime time;
                    if (this._options.GetTimeFunc != null)
                    {
                        time = this._options.GetTimeFunc();
                    }
                    else
                    {
                        time = DateTime.Now;
                    }

                    string timeStr;
                    if (this._options.TimeFormatFunc != null)
                    {
                        timeStr = this._options.TimeFormatFunc(time);
                    }
                    else
                    {
                        timeStr = time.ToString("yyyy-MM-dd HH:mm:ss");
                    }

                    if (this._lastDrawTarget == PlotLastDrawTarget.Bitmap2)
                    {
                        this.DrawTimeText(rp, rect, this._bitmap2, this._bitmap1, this._bitmap1Canvas, step, timeStr);
                        this._lastDrawTarget = PlotLastDrawTarget.Bitmap1;
                    }
                    else
                    {
                        this.DrawTimeText(rp, rect, this._bitmap1, this._bitmap2, this._bitmap2Canvas, step, timeStr);
                        this._lastDrawTarget = PlotLastDrawTarget.Bitmap2;
                    }

                    this._totalStep = 0;
                }
                else
                {
                    if (this._lastDrawTarget == PlotLastDrawTarget.Bitmap2)
                    {
                        this.DrawTimeText(rp, rect, this._bitmap2, this._bitmap1, this._bitmap1Canvas, step, null);
                        this._lastDrawTarget = PlotLastDrawTarget.Bitmap1;
                    }
                    else
                    {
                        this.DrawTimeText(rp, rect, this._bitmap1, this._bitmap2, this._bitmap2Canvas, step, null);
                        this._lastDrawTarget = PlotLastDrawTarget.Bitmap2;
                    }
                }
            }
        }

        private void DrawTimeText(RenderPack rp, SKRect plotDestRect, SKBitmap lastBitmap,
            SKBitmap currentBitmap, SKCanvas currentCanvas, int step, string timeStr)
        {
            SKPaint textPaint = this._paint;
            int edgeDistance = this._options.EdgeDistance;
            int moveSize;
            SKRect sourceRect, destRect;
            float x, y;
            currentCanvas.Clear();

            switch (this._moveOrientation)
            {
                case PlotMoveDirection.Up:
                    moveSize = lastBitmap.Height - step;
                    sourceRect = new SKRect(0, step, lastBitmap.Width, lastBitmap.Height);
                    destRect = new SKRect(0, 0, lastBitmap.Width, moveSize);
                    currentCanvas.DrawBitmap(lastBitmap, sourceRect, destRect);

                    if (!string.IsNullOrWhiteSpace(timeStr))
                    {
                        if (this._dockDrection == PlotDockDirection.Left)
                        {
                            x = edgeDistance;
                            y = moveSize;
                        }
                        else
                        {
                            x = lastBitmap.Width - edgeDistance - this._textBounds.Width;
                            y = moveSize;
                        }

                        currentCanvas.DrawText(timeStr, x, y, textPaint);
                    }
                    break;
                case PlotMoveDirection.Down:
                    moveSize = lastBitmap.Height - step;
                    sourceRect = new SKRect(0, 0, lastBitmap.Width, moveSize);
                    destRect = new SKRect(0, step, lastBitmap.Width, lastBitmap.Height);
                    currentCanvas.DrawBitmap(lastBitmap, sourceRect, destRect);

                    if (!string.IsNullOrWhiteSpace(timeStr))
                    {
                        if (this._dockDrection == PlotDockDirection.Left)
                        {
                            if (edgeDistance < rp.DataRect.Left)
                            {
                                x = rp.DataRect.Left;
                            }
                            else
                            {
                                x = edgeDistance;
                            }

                            if (step < rp.DataRect.Top)
                            {
                                y = rp.DataRect.Top;
                            }
                            else
                            {
                                y = step;
                            }
                        }
                        else
                        {
                            x = lastBitmap.Width - edgeDistance - this._textBounds.Width;
                            if (step < rp.DataRect.Top)
                            {
                                y = rp.DataRect.Top;
                            }
                            else
                            {
                                y = step;
                            }
                        }

                        currentCanvas.DrawText(timeStr, x, y, textPaint);
                    }
                    break;
                case PlotMoveDirection.Left:
                    moveSize = lastBitmap.Width - step;
                    sourceRect = new SKRect(step, 0, lastBitmap.Width, lastBitmap.Height);
                    destRect = new SKRect(0, 0, moveSize, lastBitmap.Height);
                    currentCanvas.DrawBitmap(lastBitmap, sourceRect, destRect);

                    if (!string.IsNullOrWhiteSpace(timeStr))
                    {
                        currentCanvas.RotateDegrees(-90, 0, 0);
                        currentCanvas.Translate(-lastBitmap.Height, 0);
                        currentCanvas.Save();

                        if (this._dockDrection == PlotDockDirection.Bottom)
                        {
                            x = edgeDistance;
                        }
                        else
                        {
                            x = lastBitmap.Height - this._textBounds.Width - edgeDistance;
                        }
                        y = moveSize;

                        currentCanvas.DrawText(timeStr, x, y, textPaint);
                        currentCanvas.Flush();

                        currentCanvas.ResetMatrix();//重置变换
                        currentCanvas.Save();
                    }
                    break;
                case PlotMoveDirection.Right:
                    moveSize = lastBitmap.Width - step;
                    sourceRect = new SKRect(0, 0, moveSize, lastBitmap.Height);
                    destRect = new SKRect(step, 0, lastBitmap.Width, lastBitmap.Height);
                    currentCanvas.DrawBitmap(lastBitmap, sourceRect, destRect);

                    if (!string.IsNullOrWhiteSpace(timeStr))
                    {
                        currentCanvas.RotateDegrees(90, 0, 0);
                        currentCanvas.Translate(0, -lastBitmap.Width);
                        currentCanvas.Save();

                        if (this._dockDrection == PlotDockDirection.Bottom)
                        {
                            x = lastBitmap.Height - this._textBounds.Width - edgeDistance;
                        }
                        else
                        {
                            x = edgeDistance;
                        }
                        y = moveSize;

                        currentCanvas.DrawText(timeStr, x, y, textPaint);
                        currentCanvas.Flush();

                        currentCanvas.ResetMatrix();//重置变换
                        currentCanvas.Save();
                    }
                    break;
                default:
                    throw new NotSupportedException($"不支持的移动方向{this._moveOrientation.ToString()}");
            }

            currentCanvas.Flush();
            rp.Canvas.DrawBitmap(currentBitmap, plotDestRect);
        }

        private bool IsDrawTimeText(int totalStep)
        {
            if (totalStep < this._options.StepLimit)
            {
                return false;
            }

            switch (this._moveOrientation)
            {
                case PlotMoveDirection.Up:
                case PlotMoveDirection.Down:
                    if (totalStep < this._textBounds.Height)
                    {
                        return false;
                    }
                    break;
                case PlotMoveDirection.Left:
                case PlotMoveDirection.Right:
                    if (totalStep < this._textBounds.Width)
                    {
                        return false;
                    }
                    break;
                default:
                    throw new NotSupportedException($"不支持的移动方向{this._moveOrientation.ToString()}");
            }

            return true;
        }


        /// <summary>
        /// 
        /// </summary>
        public IEnumerable<LegendItem> LegendItems
        {
            get { return Enumerable.Empty<LegendItem>(); }
        }

        #endregion

    }

    /// <summary>
    /// 瀑布图时间标注参数
    /// </summary>
    public class TimePlotOptions
    {
        /// <summary>
        /// 默认可选项参数
        /// </summary>
        public static readonly TimePlotOptions Default = null;

        static TimePlotOptions()
        {
            Default = new TimePlotOptions();
        }



        /// <summary>
        /// 构造函数初始化
        /// </summary>
        public TimePlotOptions()
        {

        }


        /// <summary>
        /// 是否启用时间标注[true:启用;false:禁用]
        /// </summary>
        public bool Enable { get; set; } = true;

        /// <summary>
        /// 对齐选项参数
        /// </summary>
        public PlotAlignedExtentOptions AlignedExtentOptions { get; set; } = null;

        /// <summary>
        /// If true, pixels in the final image will be interpolated to give the heatmap a smooth appearance.
        /// If false, the heatmap will appear as individual rectangles with sharp edges.
        /// </summary>
        public bool Smooth { get; set; } = false;

        /// <summary>
        /// 获取标注时间回调,为null时使用本机当前时间
        /// </summary>
        public Func<DateTime> GetTimeFunc { get; set; } = null;

        /// <summary>
        /// 时间标注格式化回调,为null时使用默认
        /// </summary>
        public Func<DateTime, string> TimeFormatFunc { get; set; } = null;

        /// <summary>
        /// 获取水平或垂直方向上时间标注步进像素值回调,不能为null
        /// </summary>
        public Func<int> GetStepFunc { get; set; } = null;

        /// <summary>
        /// 时间标注文本颜色
        /// </summary>
        public SKColor TextColor { get; set; } = SKColors.Red;

        /// <summary>
        /// 时间标注文本字体
        /// </summary>
        public SKTypeface Font { get; set; } = null;

        /// <summary>
        /// 时间标注文本大小
        /// </summary>
        public float FontSize { get; set; } = 15;

        /// <summary>
        /// 时间标注文本大小
        /// </summary>
        public SKTextAlign TextAlign { get; set; } = SKTextAlign.Left;

        /// <summary>
        /// 是否启用抗锯齿[true;启用;flase:禁用]
        /// </summary>
        public bool IsAntialias { get; set; } = false;

        /// <summary>
        /// 数据步进门限,当累计步长大于等于此值时绘制一次时间
        /// </summary>
        public int StepLimit { get; set; } = 100;

        /// <summary>
        /// 时间标注文本边距
        /// </summary>
        public int EdgeDistance { get; set; } = 10;

    }

}
